package com.study.transaction_1;

import com.study.util.JDBCUtils;
import org.junit.Test;

import java.lang.reflect.Field;
import java.sql.*;

/*
 * 1.什么叫数据库事务？
 * 事务：一组逻辑操作单元,使数据从一种状态变换到另一种状态。
 * 		> 一组逻辑操作单元：一个或多个DML(增删改)操作。
 *
 * 2.事务处理的原则：保证所有事务都作为一个工作单元来执行，即使出现了故障，都不能改变这种执行方式。
 * 当在一个事务中执行多个操作时，要么所有的事务都被提交(commit)，那么这些修改就永久地保存
 * 下来；要么数据库管理系统将放弃所作的所有修改，整个事务回滚(rollback)到最初状态。
 *
 * 3.数据一旦提交，就不可回滚
 *
 * DDL操作：1）新增/删除数据库 2）删除数据表 3）数据表中新增/删除字段 4）修改表名 5）修改字段：名称、类型、长度、约束描述等
 * DML操作：1）输入数据 2）删除数据 3）更新数据
 *
 * 4.哪些操作会导致数据的自动提交？
 * 		>DDL操作一旦执行，都会自动提交。
 * 			>set autocommit = false 对DDL操作失效
 * 		>DML默认情况下，一旦执行，就会自动提交。
 * 			>我们可以通过set autocommit = false的方式取消DML操作的自动提交。
 * 		>默认在关闭连接时，会自动的提交数据【重点，将同一事务中的数据操作写到一个连接中才可以进行回滚操作】
 */
public class TransactionTest {

    //******************未考虑数据库事务情况下的转账操作**************************
    /*
     * 针对于数据表user_table来说：
     * AA用户给BB用户转账100
     *
     * update user_table set balance = balance - 100 where user = 'AA';
     * update user_table set balance = balance + 100 where user = 'BB';
     */
    @Test
    public void testUpdate() {
        String sql1 = "update user_table set balance = balance - 100 where user = ?";
        update(sql1, "AA");
        //模拟网络异常
        System.out.println(10 / 0);
        String sql2 = "update user_table set balance = balance + 100 where user = ?";
        update(sql2, "BB");
        System.out.println("转账成功");
        // 无异常的情况下输出：转账成功  AA：1000 -》900 BB：2000 -》2100
        // 模拟网络异常的情况下输出：java.lang.ArithmeticException: / by zero
        //                             AA：1000 -》900 BB：2000
    }

    // 通用的增删改操作---version 1.0
    // sql中占位符的个数与可变形参的长度相同！
    public int update(String sql, Object... args) {
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            // 1.获取数据库的连接
            conn = JDBCUtils.getConnection();
            // 2.预编译sql语句，返回PreparedStatement的实例
            ps = conn.prepareStatement(sql);
            // 3.填充占位符
            for (int i = 0; i < args.length; i++) {
                ps.setObject(i + 1, args[i]);// 小心参数声明错误！！
            }
            // 4.执行
            return ps.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //修改其为自动提交数据
            //主要针对于使用数据库连接池的使用
            try {
                conn.setAutoCommit(true);
            } catch (SQLException e) {
                e.printStackTrace();
            }
            // 5.资源的关闭
            JDBCUtils.closeResource(conn, ps);
        }
        return 0;
    }

    //********************考虑数据库事务后的转账操作*********************
    @Test
    public void testUpdateWithTx() {
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            // 输出：数据的自动提交1：true
            System.out.println("数据的自动提交1：" + conn.getAutoCommit());
            //1.取消数据的自动提交【默认在关闭连接时，会自动的提交数据】
            conn.setAutoCommit(false);
            // 输出：数据的自动提交2：false
            System.out.println("数据的自动提交2：" + conn.getAutoCommit());
            String sql1 = "update user_table set balance = balance - 100 where user = ?";
            update(conn, sql1, "AA");
            //模拟网络异常
            System.out.println(10 / 0);
            String sql2 = "update user_table set balance = balance + 100 where user = ?";
            update(conn, sql2, "BB");
            System.out.println("转账成功");
            //2.提交数据
            conn.commit();
            // 无异常的情况下输出：转账成功  AA：1000 -》900 BB：2000 -》2100
            // 模拟网络异常的情况下输出：java.lang.ArithmeticException: / by zero
            //                             AA：1000  BB：2000
        } catch (Exception e) {
            e.printStackTrace();
            //3.回滚数据
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        } finally {
            JDBCUtils.closeResource(conn, null);
        }
    }

    // 通用的增删改操作---version 2.0 （考虑上事务）
    public int update(Connection conn, String sql, Object... args) {// sql中占位符的个数与可变形参的长度相同！
        PreparedStatement ps = null;
        try {
            // 1.预编译sql语句，返回PreparedStatement的实例
            ps = conn.prepareStatement(sql);
            // 2.填充占位符
            for (int i = 0; i < args.length; i++) {
                ps.setObject(i + 1, args[i]);// 小心参数声明错误！！
            }
            // 3.执行
            return ps.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //修改其为自动提交数据
            //主要针对于使用数据库连接池的使用
            try {
                conn.setAutoCommit(true);
            } catch (SQLException e) {
                e.printStackTrace();
            }
            // 4.资源的关闭
            JDBCUtils.closeResource(null, ps);
        }
        return 0;
    }

    //****************事务的ACID属性及4种隔离级别***************************
/*
一、事务的ACID属性
    1）原子性（Atomicity）：原子性是指事务是一个不可分割的工作单位，事务中的操作要么都发生，要么都不发生
    2）一致性（Consistency）：事务必须使数据库从一个一致性状态变换到另外一个一致性状态
    【一致性举例：1000 -》900 1000 -》1100，但总和仍是2000】
    3）隔离性（Isolation）：事务的隔离性是指一个事务的执行不能被其他事务干扰，即一个事务内部的操作及使用的
数据对并发的其他事务是隔离的，并发执行的各个事务之间不能互相干扰【数据库并发问题】
    4）持久性（Durability）：持久性是指一个事务一旦被提交，它对数据库中数据的改变就是永久性的，接下来的其
他操作和数据库故障不应该对其有任何影响

二、数据库的并发问题
对于同时运行的多个事务, 当这些事务访问数据库中相同的数据时, 如果没有采取必要的隔离机制, 就会导致各种并发问题:
1）脏读: 对于两个事务 T1, T2, T1 读取了已经被 T2 更新但还没有被提交的字段。之后, 若 T2 回滚, T1读取的
内容就是临时且无效的。
2）不可重复读: 对于两个事务T1, T2, T1 读取了一个字段, 然后 T2 更新了该字段。之后, T1再次读取同一个字
段, 值就不同了。
3）幻读: 对于两个事务T1, T2, T1 从一个表中读取了一个字段, 然后 T2 在该表中插入了一些新的行。之后, 如
果 T1 再次读取同一个表, 就会多出几行。

三、四种隔离级别
数据库事务的隔离性: 数据库系统必须具有隔离并发运行各个事务的能力, 使它们不会相互影响, 避免各种并发问
题。
一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别, 不同隔离级别对应不同的干扰
程度, 隔离级别越高, 数据一致性就越好, 但并发性越弱。

四种隔离级别见doc文件夹下的 四种隔离级别.jpg

Oracle 支持的 2 种事务隔离级别：READ COMMITED, SERIALIZABLE
Oracle 默认的事务隔离级别为: READ COMMITED 。

Mysql 支持 4 种事务隔离级别。Mysql 默认的事务隔离级别为: REPEATABLE READ
*/

    // 事务的隔离级别 查询测试
    @Test
    public void testTransactionSelect() throws Exception {
        Connection conn = JDBCUtils.getConnection();
        //获取当前连接的隔离级别--TRANSACTION_REPEATABLE_READ 可重复读
        System.out.println("当前连接的隔离级别" + conn.getTransactionIsolation());
        //输出：当前连接的隔离级别4  --》TRANSACTION_REPEATABLE_READ
        //设置数据库的隔离级别：
        conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
        //输出：设置的隔离级别2 --》TRANSACTION_READ_COMMITTED  读已提交数据
        System.out.println("设置的隔离级别" + conn.getTransactionIsolation());
        //取消自动提交数据
        conn.setAutoCommit(false);
        String sql = "select user,password,balance from user_table where user = ?";
        User user = getInstance(conn, User.class, sql, "CC");
        System.out.println(user);
    }

    // 事务的隔离级别 更新测试
    @Test
    public void testTransactionUpdate() throws Exception {
        Connection conn = JDBCUtils.getConnection();
        //取消自动提交数据
        conn.setAutoCommit(false);
        String sql = "update user_table set balance = ? where user = ?";
        update(conn, sql, 5000, "CC");

        Thread.sleep(10000);
        System.out.println("修改结束");
    }

    //通用的查询操作，用于返回数据表中的一条记录（version 2.0：考虑上事务）
    public <T> T getInstance(Connection conn, Class<T> clazz, String sql, Object... args) {
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < args.length; i++) {
                ps.setObject(i + 1, args[i]);
            }
            rs = ps.executeQuery();
            // 获取结果集的元数据 :ResultSetMetaData
            ResultSetMetaData rsmd = rs.getMetaData();
            // 通过ResultSetMetaData获取结果集中的列数
            int columnCount = rsmd.getColumnCount();
            if (rs.next()) {
                T t = clazz.newInstance();
                // 处理结果集一行数据中的每一个列
                for (int i = 0; i < columnCount; i++) {
                    // 获取列值
                    Object columValue = rs.getObject(i + 1);

                    // 获取每个列的列名
                    // String columnName = rsmd.getColumnName(i + 1);
                    String columnLabel = rsmd.getColumnLabel(i + 1);

                    // 给t对象指定的columnName属性，赋值为columValue：通过反射
                    Field field = clazz.getDeclaredField(columnLabel);
                    field.setAccessible(true);
                    field.set(t, columValue);
                }
                return t;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.closeResource(null, ps, rs);
        }
        return null;
    }
}
